package cn.jetpiece.cloud.joss.app.modular.storage.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.jetpiece.cloud.core.util.HttpContext;
import cn.jetpiece.cloud.joss.api.object.model.enums.AclEnum;
import cn.jetpiece.cloud.joss.api.object.model.params.CompleteMultiPartUploadObject;
import cn.jetpiece.cloud.joss.api.object.model.params.InitMultiPartUploadObject;
import cn.jetpiece.cloud.joss.api.object.model.params.MultiPartUploadObject;
import cn.jetpiece.cloud.joss.api.object.model.result.JossPartEtag;
import cn.jetpiece.cloud.joss.api.object.model.result.UploadObjectResult;
import cn.jetpiece.cloud.joss.app.core.constant.OssConstants;
import cn.jetpiece.cloud.joss.app.modular.basics.entity.StorageClass;
import cn.jetpiece.cloud.joss.app.modular.object.entity.Bucket;
import cn.jetpiece.cloud.joss.app.modular.object.entity.Provider;
import cn.jetpiece.cloud.joss.app.modular.object.entity.Region;
import cn.jetpiece.cloud.joss.app.modular.object.enums.BasicExceptionEnum;
import cn.jetpiece.cloud.joss.app.modular.object.model.BucketInfo;
import cn.jetpiece.cloud.joss.app.modular.storage.service.AbstractJossClient;
import cn.jetpiece.cloud.model.exception.ServiceException;
import com.obs.services.ObsClient;
import com.obs.services.exception.ObsException;
import com.obs.services.model.*;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.http.parser.Ranges;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLDecoder;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Slf4j
public class ObsJossClient extends AbstractJossClient {
    private final ObsClient client;

    public ObsJossClient(String endpoint, String accessKey, String secretKey) {
        super(endpoint, accessKey, secretKey);
        client = new ObsClient(accessKey, secretKey, endpoint);
    }

    @Override
    public void createBucket(Provider provider, Region region, Bucket bucket, StorageClass storageClass) {
        try {
            ObsBucket obsBucket = new ObsBucket();
            obsBucket.setBucketName(bucket.getBucketName());
            obsBucket.setLocation(region.getEnName());
            obsBucket.setBucketStorageClass(StorageClassEnum.getValueFromCode(storageClass.getStorageClassValue()));
            obsBucket.setCreationDate(bucket.getCreateTime());
            if (bucket.getAcl() == AclEnum.PRIVATE.getCode()) {
                obsBucket.setAcl(AccessControlList.REST_CANNED_PRIVATE);
            } else if (bucket.getAcl() == AclEnum.PUBLIC_READ.getCode()) {
                obsBucket.setAcl(AccessControlList.REST_CANNED_PUBLIC_READ);
            } else if (bucket.getAcl() == AclEnum.PUBLIC_READ_WRITE.getCode()) {
                obsBucket.setAcl(AccessControlList.REST_CANNED_PUBLIC_READ_WRITE);
            }
            CreateBucketRequest request = new CreateBucketRequest();
            request.setBucketName(bucket.getBucketName());
            request.setAcl(obsBucket.getAcl());
            request.setBucketStorageClass(obsBucket.getBucketStorageClass());
            request.setLocation(obsBucket.getLocation());
            if (bucket.getDataRedundancyType() == 2) {
                request.setAvailableZone(AvailableZoneEnum.MULTI_AZ);
            }
            client.createBucket(request);
        } catch (ObsException e) {
            log.error(e.getXmlMessage(), e);
            throw new ServiceException(BasicExceptionEnum.BUCKET_CREATE_ERROR);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new ServiceException(BasicExceptionEnum.BUCKET_CREATE_ERROR);
        }
    }

    @Override
    public void deleteBucket(BucketInfo bucketInfo) {
        try {
            client.deleteBucket(bucketInfo.getBucketName());
        } catch (ObsException e) {
            log.error(e.getXmlMessage(), e);
            throw new ServiceException(500,"删除桶失败");
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new ServiceException(500,"删除桶失败");
        }
    }

    @Override
    public String initMultipartUpload(BucketInfo bucketInfo, String objectName, InitMultiPartUploadObject param) {
        try {
            String objectNameTemp;
            if (objectName.startsWith(OssConstants.PATH_SEPARATE)) {
                objectNameTemp = objectName.substring(1);
            } else {
                objectNameTemp = objectName;
            }
            InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketInfo.getBucketName(), objectNameTemp);
            if (param.getAcl() == AclEnum.PRIVATE.getCode()) {
                request.setAcl(AccessControlList.REST_CANNED_PRIVATE);
            } else if (param.getAcl() == AclEnum.PUBLIC_READ.getCode()) {
                request.setAcl(AccessControlList.REST_CANNED_PUBLIC_READ);
            } else if (param.getAcl() == AclEnum.PUBLIC_READ_WRITE.getCode()) {
                request.setAcl(AccessControlList.REST_CANNED_PUBLIC_READ_WRITE);
            }
            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setContentType(param.getContentType());
            request.setMetadata(metadata);
            return client.initiateMultipartUpload(request).getUploadId();
        } catch (ObsException e) {
            log.error(e.getXmlMessage(), e);
            throw new ServiceException(BasicExceptionEnum.OBJECT_UPLOAD_ERROR);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new ServiceException(BasicExceptionEnum.OBJECT_UPLOAD_ERROR);
        }
    }

    @Override
    public JossPartEtag multipartUpload(BucketInfo bucketInfo, String objectName, MultiPartUploadObject param, MultipartFile file) {
        try {
            String objectNameTemp;
            if (objectName.startsWith(OssConstants.PATH_SEPARATE)) {
                objectNameTemp = objectName.substring(1);
            } else {
                objectNameTemp = objectName;
            }
            UploadPartRequest request = new UploadPartRequest(bucketInfo.getBucketName(), objectNameTemp);
            request.setBucketName(bucketInfo.getBucketName());
            request.setObjectKey(objectNameTemp);
            request.setUploadId(param.getUploadId());
            request.setInput(file.getInputStream());

            request.setPartSize(param.getPartSize());
            request.setOffset(param.getOffset());
            request.setPartNumber(param.getPartNumber());
            request.setContentMd5(param.getContentMd5());

            UploadPartResult uploadPartResult = client.uploadPart(request);
            JossPartEtag partEtag = new JossPartEtag();
            partEtag.setEtag(uploadPartResult.getEtag());
            partEtag.setPartNumber(param.getPartNumber());
            partEtag.setPartSize(param.getPartSize());
            return partEtag;
        } catch (ObsException e) {
            log.error(e.getXmlMessage(), e);
            throw new ServiceException(BasicExceptionEnum.OBJECT_UPLOAD_ERROR);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new ServiceException(BasicExceptionEnum.OBJECT_UPLOAD_ERROR);
        }
    }

    @Override
    public UploadObjectResult completeMultipartUpload(BucketInfo bucketInfo, String objectName, CompleteMultiPartUploadObject param) {
        try {
            String objectNameTemp;
            if (objectName.startsWith(OssConstants.PATH_SEPARATE)) {
                objectNameTemp = objectName.substring(1);
            } else {
                objectNameTemp = objectName;
            }
            List<PartEtag> partEtag = param.getEtag().stream().map(item -> BeanUtil.copyProperties(item, PartEtag.class)).collect(Collectors.toList());
            CompleteMultipartUploadRequest request = new CompleteMultipartUploadRequest(bucketInfo.getBucketName(), objectNameTemp, param.getUploadId(), partEtag);
            CompleteMultipartUploadResult completeMultipartUploadResult = client.completeMultipartUpload(request);

            ObsObject obsObject = client.getObject(bucketInfo.getBucketName(), objectNameTemp);

            UploadObjectResult result = new UploadObjectResult();
            result.setBucketName(bucketInfo.getBucketName());
            result.setContextType(obsObject.getMetadata().getContentType());
            result.setObjectSize(obsObject.getMetadata().getContentLength());
            result.setObjectName(objectNameTemp);
            result.setProviderUrl(URLDecoder.decode(completeMultipartUploadResult.getObjectUrl(), "UTF-8"));
            return result;
        } catch (ObsException e) {
            log.error(e.getXmlMessage(), e);
            throw new ServiceException(BasicExceptionEnum.OBJECT_UPLOAD_ERROR);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new ServiceException(BasicExceptionEnum.OBJECT_UPLOAD_ERROR);
        }

    }

    @Override
    public String upload(BucketInfo bucketInfo, Integer acl, String objectName, File file) {
        if (acl == null) {
            acl = bucketInfo.getAcl();
        }
        try {
            /*
             * 适配华为云url多出一个/
             */
            String objectNameTemp;
            if (objectName.startsWith(OssConstants.PATH_SEPARATE)) {
                objectNameTemp = objectName.substring(1);
            } else {
                objectNameTemp = objectName;
            }
            PutObjectRequest request = new PutObjectRequest(bucketInfo.getBucketName(), objectNameTemp, file);
            if (acl == AclEnum.PRIVATE.getCode()) {
                request.setAcl(AccessControlList.REST_CANNED_PRIVATE);
            } else if (acl == AclEnum.PUBLIC_READ.getCode()) {
                request.setAcl(AccessControlList.REST_CANNED_PUBLIC_READ);
            } else if (acl == AclEnum.PUBLIC_READ_WRITE.getCode()) {
                request.setAcl(AccessControlList.REST_CANNED_PUBLIC_READ_WRITE);
            }
            PutObjectResult objectResult = client.putObject(request);
            return URLDecoder.decode(objectResult.getObjectUrl(), "UTF-8");
        } catch (ObsException e) {
            log.error(e.getXmlMessage(), e);
            throw new ServiceException(BasicExceptionEnum.OBJECT_UPLOAD_ERROR);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new ServiceException(BasicExceptionEnum.OBJECT_UPLOAD_ERROR);
        }
    }

    @Override
    public String upload(BucketInfo bucketInfo, Integer acl, String objectName, long objectSize, String contentType, byte[] objectBytes) {
        return upload(bucketInfo, acl, objectName, objectSize, contentType, new ByteArrayInputStream(objectBytes));
    }

    @Override
    public String upload(BucketInfo bucketInfo, Integer acl, String objectName, long objectSize, String contentType, InputStream inputStream) {
        if (acl == null) {
            acl = bucketInfo.getAcl();
        }
        try {
            String objectNameTemp;
            if (objectName.startsWith(OssConstants.PATH_SEPARATE)) {
                objectNameTemp = objectName.substring(1);
            } else {
                objectNameTemp = objectName;
            }
            PutObjectRequest request = new PutObjectRequest(bucketInfo.getBucketName(), objectNameTemp, inputStream);
            if (acl == AclEnum.PRIVATE.getCode()) {
                request.setAcl(AccessControlList.REST_CANNED_PRIVATE);
            } else if (acl == AclEnum.PUBLIC_READ.getCode()) {
                request.setAcl(AccessControlList.REST_CANNED_PUBLIC_READ);
            } else if (acl == AclEnum.PUBLIC_READ_WRITE.getCode()) {
                request.setAcl(AccessControlList.REST_CANNED_PUBLIC_READ_WRITE);
            }
            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setContentType(contentType);
            request.setMetadata(metadata);
            PutObjectResult objectResult = client.putObject(request);
            return URLDecoder.decode(objectResult.getObjectUrl(), "UTF-8");
        } catch (ObsException e) {
            log.error(e.getXmlMessage(), e);
            throw new ServiceException(BasicExceptionEnum.OBJECT_UPLOAD_ERROR);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new ServiceException(BasicExceptionEnum.OBJECT_UPLOAD_ERROR);
        }
    }

    @Override
    public void delete(BucketInfo bucketInfo, String objectName, boolean checkAcl) {
        try {
            String objectNameTemp = objectName;
            if (!client.doesObjectExist(bucketInfo.getBucketName(), objectName)) {
                objectNameTemp = objectName.substring(1);
                if (!client.doesObjectExist(bucketInfo.getBucketName(), objectNameTemp)) {
                    objectNameTemp = OssConstants.PATH_SEPARATE + objectName;
                    if (!client.doesObjectExist(bucketInfo.getBucketName(), objectNameTemp)) {
                        throw new ServiceException(BasicExceptionEnum.NOT_FOUND);
                    }
                }
            }
            if (checkAcl) {
                AccessControlList accessControlList = client.getObjectAcl(bucketInfo.getBucketName(), objectNameTemp);
                if (accessControlList == null) {
                    throw new ServiceException(BasicExceptionEnum.FORBIDDEN);
                }
                Set<GrantAndPermission> grantAndPermissions = accessControlList.getGrants();
                if (grantAndPermissions.isEmpty()) {
                    throw new ServiceException(BasicExceptionEnum.FORBIDDEN);
                }
                boolean permission = false;
                for (GrantAndPermission grantAndPermission : grantAndPermissions) {
                    if (grantAndPermission.getGrantee().equals(GroupGrantee.ALL_USERS) && !grantAndPermission.getPermission().equals(Permission.PERMISSION_FULL_CONTROL)) {
                        permission = true;
                    }
                }
                if (!permission) {
                    throw new ServiceException(BasicExceptionEnum.FORBIDDEN);
                }
            }
            client.deleteObject(bucketInfo.getBucketName(), objectNameTemp);
        } catch (ObsException e) {
            log.error(e.getXmlMessage(), e);
            throw new ServiceException(BasicExceptionEnum.OBJECT_UPLOAD_ERROR);
        } catch (ServiceException e) {
            throw e;
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new ServiceException(BasicExceptionEnum.OBJECT_DELETE_ERROR);
        }
    }

    @SneakyThrows
    @Override
    public void getObject(BucketInfo bucketInfo, String objectName, boolean checkAcl) {
        HttpServletRequest request = HttpContext.getRequest();
        HttpServletResponse response = HttpContext.getResponse();
        try {
            String objectNameTemp = objectName;
            if (objectName.startsWith(OssConstants.PATH_SEPARATE)) {
                objectNameTemp = objectName.substring(1);
            }
            if (objectNameTemp.startsWith(OssConstants.PATH_SEPARATE)) {
                objectNameTemp = objectNameTemp.substring(1);
            }
            if (!client.doesObjectExist(bucketInfo.getBucketName(), objectNameTemp)) {
                objectNameTemp = OssConstants.PATH_SEPARATE + objectNameTemp;
                if (!client.doesObjectExist(bucketInfo.getBucketName(), objectNameTemp)) {
                    throw new ServiceException(BasicExceptionEnum.OBJECT_PREVIEW_ERROR);
                }
            }
            if (checkAcl) {
                AccessControlList accessControlList = client.getObjectAcl(bucketInfo.getBucketName(), objectNameTemp);
                if (accessControlList == null) {
                    throw new ServiceException(BasicExceptionEnum.FORBIDDEN);
                }
                Set<GrantAndPermission> grantAndPermissions = accessControlList.getGrants();
                if (grantAndPermissions.isEmpty()) {
                    throw new ServiceException(BasicExceptionEnum.FORBIDDEN);
                }
                boolean permission = false;
                for (GrantAndPermission grantAndPermission : grantAndPermissions) {
                    if (grantAndPermission.getGrantee().equals(GroupGrantee.ALL_USERS) && !grantAndPermission.getPermission().equals(Permission.PERMISSION_FULL_CONTROL)) {
                        permission = true;
                    }
                }
                if (!permission) {
                    throw new ServiceException(BasicExceptionEnum.FORBIDDEN);
                }
            }
            Ranges ranges = getRanges(request);
            if (ranges == null) {
                GetObjectRequest getObjectRequest = new GetObjectRequest(bucketInfo.getBucketName(), objectNameTemp);
                String imageProcess = HttpContext.getRequestParameters().get("x-image-process");
                if (StrUtil.isNotBlank(imageProcess)) {
                    getObjectRequest.setImageProcess(imageProcess);
                }
                GetObjectMetadataRequest metadataRequest = new GetObjectMetadataRequest(bucketInfo.getBucketName(),objectNameTemp);
                ObjectMetadata objectMetadata = client.getObjectMetadata(metadataRequest);
                String ifNoneMatch = request.getHeader("If-None-Match");
                String ifModifiedSince = request.getHeader("If-Modified-Since");
                String etag = objectMetadata.getEtag();
                String lastModified = DateUtil.format(objectMetadata.getLastModified(), DatePattern.HTTP_DATETIME_FORMAT);
                response.setContentType(objectMetadata.getContentType());
                response.setHeader("ETag", etag);
                response.setHeader("Last-Modified", lastModified);
                if (StrUtil.isAllNotBlank(ifNoneMatch,ifModifiedSince) && etag.equals(ifNoneMatch) && ifModifiedSince.equals(lastModified)) {
                    response.setContentLengthLong(0L);
                    response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                } else {
                    ObsObject obsObject = client.getObject(getObjectRequest);
                    response.setHeader("Accept-Ranges", "bytes");
                    response.setContentLengthLong(obsObject.getMetadata().getContentLength());
                    OutputStream outputStream = response.getOutputStream();
                    copyIO(obsObject.getObjectContent(), outputStream, obsObject.getMetadata().getContentLength());
                }
            } else {
                ObjectMetadata objectMetadata = client.getObjectMetadata(bucketInfo.getBucketName(), objectNameTemp);
                long startRange = ranges.getEntries().get(0).getStart();
                long endRange = ranges.getEntries().get(0).getEnd();
                if (endRange == -1) {
                    endRange = objectMetadata.getContentLength() - 1;
                }
                GetObjectRequest getObjectRequest = new GetObjectRequest(bucketInfo.getBucketName(), objectNameTemp);
                getObjectRequest.setRangeStart(startRange);
                getObjectRequest.setRangeEnd(endRange);
                ObsObject obsObject = client.getObject(getObjectRequest);
                long contentLength = obsObject.getMetadata().getContentLength();

                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
                response.setContentType(obsObject.getMetadata().getContentType());
                response.setHeader("Accept-Ranges", "bytes");
                response.setHeader("Content-Range", ranges.getUnits() + " " + startRange + "-" + endRange + "/" + objectMetadata.getContentLength());
                response.setHeader("ETag", obsObject.getMetadata().getEtag());
                response.setHeader("Last-Modified", DateUtil.format(obsObject.getMetadata().getLastModified(), DatePattern.HTTP_DATETIME_PATTERN));
                response.setContentLengthLong(contentLength);
                OutputStream outputStream = response.getOutputStream();
                copyIO(obsObject.getObjectContent(), outputStream, obsObject.getMetadata().getContentLength());
            }
        } catch (ObsException e) {
            log.error(e.getXmlMessage(), e);
            throw new ServiceException(BasicExceptionEnum.OBJECT_UPLOAD_ERROR);
        } catch (ServiceException e) {
            throw e;
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new ServiceException(BasicExceptionEnum.OBJECT_PREVIEW_ERROR);
        }
    }

    @Override
    public void destroy() {
        try {
            client.close();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}
